Most games make use of external graphics libraries for rendering. The two most popular graphics libraries are DirectX and OpenGL. Both of these libraries are loaded by games dynamically. Once loaded, games then invoke functions in these libraries. For example, with OpenGL, games can make use of the glDrawElements function to draw a series of elements from data stored in an array. Since these libraries are external, the game's developers do not need to implement the rendering logic themselves.
Our goal in this lab is to create a wallhack by hooking the graphics library the game uses and modifying its logic to display entities through walls.
DirectX and OpenGL each have different functions for rendering that require different approaches to hook. Our first goal will be to identify the library the game is using. As each library has several functions to handle rendering and shading, we will then need to find a function that is used by the game for rendering. With the function identified, we can then hook it and disable depth-testing through the use of a codecave. This will cause all entities to be rendered regardless of where they are in the 3D world.
Since graphics libraries are loaded dynamically, they must expose their functions to the main executable. Most debuggers allow you to view all the libraries loaded into an executable when attached. In x64dbg, this information is collected under the "Symbols" tab.
As we can see in the highlighted elements, opengl32.dll is being loaded into the game's process. By selecting the OpenGL module, we can see that it exports many drawing related functions. From this information, we can conclude that this game is using OpenGL to render its graphics.
There are rendering approaches in OpenGL and different games will use different approaches. For example, some older games may use glBegin, glVertex, and glEnd whereas some games may use glDrawArrays and others may use glDrawElements. Some games may use a combination of these approaches to render different aspects, such as glDrawElements for player models and glBegin for screen effects like blood.
Typically modern games will not use glBegin, glVertex, and glEnd, as these functions are considered deprecated. As such, we will ignore these functions for now. Instead, we will first investigate glDrawElements, as this is a commonly used function. Due to how OpenGL works, we will expect this function to be called constantly if it is being used by the game.
By scrolling down to the glDrawElements export in the "Symbols" tab, we can see that OpenGL exports it to the process, though this is not a guarantee that it is being used:
By double-clicking on the export entry, x64dbg will display the function:
Next, we can start a game and then set a breakpoint on glDrawElements. You will notice it will immediately pop and pop continuously every time the game is resumed. This is a pretty good indication that this function is responsible for rendering entities. To verify that this is the case, we can replace the first instruction with the "ret" statement we see at the end of the function. The effect of this will be to immediately return to the calling code without executing any of the glDrawElements logic:
If you then resume execution and attempt to play the game, you will notice that no new entities are being rendered to the screen:
This gives us strong proof that Urban Terror is using glDrawElements to display entities.
Examining the glDrawElements function, we can see that it has very few instructions. Given the complexity of rendering entities to a screen, the majority of the code must be contained in the two calls near the end of the function:
Therefore, if we hook an instruction before these calls, we should be able to accomplish our goal of disabling depth-testing. A good candidate instruction is the mov at 0x61B9C526.
Since OpenGL is loaded dynamically, our hooking approach will have to be slightly different. First, we will need to ensure that OpenGL is actually loaded. As we are injecting our DLL into the application when it is first started, this will not be the case. After we ensure that OpenGL is loaded, we will then need to figure out where it is loaded. Once we determine the base address of OpenGL, we can then determine where glDrawEntities is located inside the OpenGL module.
To accomplish this, we will use a combination of techniques we explored in previous labs. To deal with the fact that OpenGL will not be loaded when our DLL is injected, we will create a thread to handle the hooking logic. This will allow us to create an infinite loop that waits until OpenGL is loaded, similar to the thread we saw in our gold internal hack:
if (fdwReason == DLL_PROCESS_ATTACH) {
CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)injected_thread, NULL, 0, NULL);
}In our thread, we will create an infinite loop that will call GetModuleHandle. This API returns the module handle, or base address, for a loaded module. If the module is not loaded, it will return NULL:
HMODULE openGLHandle = NULL;
void injected_thread() {
while (true) {
if (openGLHandle == NULL) {
openGLHandle = GetModuleHandle(L"opengl32.dll");
}
...
Sleep(1);When we have the base address of OpenGL, we can then locate where glDrawElements is located. To do this, we will make use of the GetProcAddress API. Given a module and the name of a function, this API returns the address of the function:
unsigned char* hook_location; ... if (openGLHandle != NULL) { hook_location = (unsigned char*)GetProcAddress(openGLHandle, "glDrawElements");
This API will return the location of the first instruction in the function, in this case "nop". Since we want to hook the mov instruction, we can subtract its distance from the first instruction and then add that difference to the result of GetProcAddress. The distance between these two instructions will always be the same, as they are part of the library's code and not loaded dynamically.
This offset can be added directly to the hook_location variable to get our location:
hook_location += 0x16;
Finally, we can hook the code as we have done in previous tutorials:
VirtualProtect((void*)hook_location, 5, PAGE_EXECUTE_READWRITE, &old_protect);
*hook_location = 0xE9;
*(DWORD*)(hook_location + 1) = (DWORD)&codecave - ((DWORD)hook_location + 5);
*(hook_location + 5) = 0x90;With glDrawElements hooked, we can start working on the codecave. Our goal is to disable depth-testing when an element is being drawn. To do this, we can use an OpenGL function called glDepthFunc. glDepthFunc allows you to set the function used for depth comparisons when OpenGL attempts to render the screen. This can be several values, but the ones we are interested in are GL_LEQUAL (draw if the element is in front of another element) and GL_ALWAYS (always draw). For our wallhack, we will set the depth function to GL_ALWAYS right before any element is drawn. This will have the effect of making all elements always appear, regardless of where in the 3D space they actually are.
First, we will need to locate glDepthFunc. To do this, we can use GetProcAddress in an identical manner to glDrawElements. However, instead of finding an address to hook, our goal with this call to GetProcAddress is to store the function's address in a manner we can then invoke in our codecave. The easiest way to do this is through a function pointer. Just like pointers we have used in previous lessons, function pointers point to an address. However, unlike the pointers we have been using to modify data and code, we can also declare a pointer to point to a function. We can then call this function, or address, like we would call any other C++ function.
To declare a function pointer, we need to know the original function's definition. The definition of a function includes its return type and its parameters. We can get this information from the Khronos group site:
void glDepthFunc(GLenum func);
Looking at the gl.h header file, we can find out what Glenum is:
typedef unsigned int GLenum;So far, we can define our glDepthFunc function like so:
void glDepthFunc(unsigned int) = NULL;
Next, we will modify this declaration to have it act as a pointer to this function:
void (*glDepthFunc)(unsigned int) = NULL;
We can now assign this to the result of GetProcAddress:
glDepthFunc = GetProcAddress(openGLHandle, "glDepthFunc");
However, if we try to build this, we will get the following error:
error C2440: '=': cannot convert from 'FARPROC' to 'void (__cdecl *)(unsigned int)'
message : This conversion requires a reinterpret_cast, a C-style cast or function-style castLike we have seen in previous labs, we need to cast the result of GetProcAddress properly for the compiler to understand how to translate the result. We can use the error message to quickly figure out how we need to cast the result:
glDepthFunc = (void(__cdecl *)(unsigned int))(openGLHandle, "glDepthFunc");Our codecave will be similar to codecaves we have written previously. We will start with our skeleton and restore the original code:
DWORD ret_address = 0; __declspec(naked) void codecave() { __asm { pushad } ... __asm { popad mov esi, dword ptr ds : [esi + 0xA18] jmp ret_address }
Unlike our previous labs, we do not have a static location to jump to. Instead, we will need to calculate our return location similar to how to we calculated the hook location. In our thread, after we assign the hook location, we can also dynamically assign the return location:
ret_address = (DWORD)(hook_location + 0x6);With our skeleton in place, we can now add in our call to glDepthFunc. First, we need to find the value for GL_ALWAYS. We can find this in the gl.h header file:
#define GL_ALWAYS 0x0207Next, we can invoke glDepthFunc to disable depth testing. Since it is a function pointer, we need to dereference the pointer to invoke the function:
(*glDepthFunc)(0x207);
Our code looks like:
#include <Windows.h> HMODULE openGLHandle = NULL; void (*glDepthFunc)(unsigned int) = NULL; unsigned char* hook_location; DWORD ret_address = 0; DWORD old_protect; __declspec(naked) void codecave() { __asm { pushad } (*glDepthFunc)(0x207); __asm { popad mov esi, dword ptr ds:[esi+0xA18] jmp ret_address } } void injected_thread() { while (true) { if (openGLHandle == NULL) { openGLHandle = GetModuleHandle(L"opengl32.dll"); } if (openGLHandle != NULL) { glDepthFunc = (void(__cdecl *)(unsigned int))GetProcAddress(openGLHandle, "glDepthFunc"); hook_location = (unsigned char*)GetProcAddress(openGLHandle, "glDrawElements"); hook_location += 0x16; VirtualProtect((void*)hook_location, 5, PAGE_EXECUTE_READWRITE, &old_protect); *hook_location = 0xE9; *(DWORD*)(hook_location + 1) = (DWORD)&codecave - ((DWORD)hook_location + 5); *(hook_location + 5) = 0x90; ret_address = (DWORD)(hook_location + 0x6); } Sleep(1); } } BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) { if (fdwReason == DLL_PROCESS_ATTACH) { CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)injected_thread, NULL, 0, NULL); } return true; }
We can now build this code and inject it into Urban Terror to see if it works.
While the console will start, when our DLL is injected, the game will crash instantly on start with the following error:
If we comment out the call to glDepthFunc in our codecave, the game no longer crashes. It looks like our function pointer is not correct in some way. If we look at gl.h, we see that glDepthFunc is defined as:
GLAPI void APIENTRY glDepthFunc (GLenum func);
In Microsoft's documentation on data types, we see that APIENTRY is a reference for WINAPI. If we look at the entry for WINAPI, we see that it is a reference for __stdcall. Let's try adding this prefix to our function pointer:
void (__stdcall *glDepthFunc)(unsigned int) = NULL;
Building this code results in a familiar error:
error C2440: '=': cannot convert from 'void (__cdecl *)(unsigned int)' to 'void (__stdcall *)(unsigned int)'This can be fixed by changing the cast as we did before:
glDepthFunc = (void(__stdcall*)(unsigned int))GetProcAddress(openGLHandle, "glDepthFunc");Calling conventions control how parameters are handled by functions when called. While there are many different types, for our purposes, we just need to know that by default, Visual Studio uses __cdecl whereas OpenGL defaults to __stdcall.
With this change, build the code and inject it again. You will notice that Urban Terror no longer crashes.
If you join a game, you will notice that you are now able to see things through walls. The only problem is that you can see too many things:
In our current hack, we are disabling depth testing for every element drawn on the screen, including walls and stairs. Ideally, we only want to draw players through walls. To accomplish this, we will have to filter out elements that we do not care about.
glDrawElements has the following definition:
void glDrawElements( GLenum mode,
GLsizei count,
GLenum type,
const void * indices);The "count" parameter specifies the amount of elements, or vertices, to be rendered. More detailed objects will have a higher amount of vertices. For example, a player model will have more detail (nose, hands, fingers, etc.) than a floor. By ensuring that the "count" parameter is a certain value, we can filter out elements we do not want to display through walls.
We know that this parameter will be on the stack when our hook is jumped to. To retrieve its exact location, we can inject our DLL and set a breakpoint on glDrawElements. As we step through the code, we can identify where it is on the stack at the time our codecave gets called.
One feature of x64dbg is the ability to view the current parameters on the stack in a similar manner to how they would be passed in C. This feature is under the panel showing the values of the registers:
x64dbg is not able to fill this in automatically, so you will need to set the calling conventions and the amount of parameters. If we trigger our breakpoint on glDrawElements multiple times, we can see that [esp+8] is the only value that appears to change. We can assume that it holds the value for the "count" parameter at the start of the function:
If we look at the stack panel at the bottom right, we can see how this information is represented on the stack. By default, the top of the stack (esp) will always appear at the top of the window:
Continue stepping through the function and then step into the jump to our codecave. After the "pushad" instruction in our codecave, examine the stack again:
At this point, we can see that the "count" parameter is at esp+0x10. We can reference this value in our codecave to retrieve the current count value of the element being rendered. In the first asm block, after the "pushad" instruction, we can grab the value of esp+0x10 and store it in a local variable:
DWORD count = 0;
...
__asm {
pushad
mov eax, dword ptr ds : [esp + 0x10]
mov count, eax
popad
pushad
}We now have a local variable "count" that will hold the value of count passed to glDrawElements. We can then compare this value to a baseline and only disable depth-testing if we exceed that baseline. If not, we will re-enable depth-testing. The value for GL_LEQUAL (0x203) can be found in the same way we found the value for GL_ALWAYS. For now, we will use 500 as a baseline value:
if (count > 500) {
(*glDepthFunc)(0x207);
}
else {
(*glDepthFunc)(0x203);
}If we build and inject this, we can see that our view is now much cleaner and only certain elements appear through walls:
While we now are filtering many elements, one big problem we have is that no player models are appearing. Instead, we can only see their weapons and blood effects through walls:
If we enable third-person view, our player model is also invisible. The only place our player model will appear is if we turn on no-clip and fly out-of-bounds. This is most likely due to our player model being drawn first when the scene is being rendered and other elements of the level then drawn on top of it. When we disable depth-testing, these entities are all drawn on top of the player.
draw_player();
draw_guns();
draw_doors();
draw_level_walls();To force players to be drawn above these elements, we can use the glDepthRange function. This function sets the near and far clipping planes for the scene. By setting these values to be equal to zero, the planes will intersect, causing all elements to be drawn on the same plane and "fight" for rendering space. This will result in some flickering, but the player models will appear through walls.
We can create a function pointer for this function identically to the approach we used for glDepthFunc. The only alterations we need to make are in the parameters:
void (__stdcall* glDepthRange)(double, double) = NULL;
...
glDepthRange = (void(__stdcall*)(double, double))GetProcAddress(openGLHandle, "glDepthRange");We can then call this function in the same location we change the depth function. The default values for these planes are (0,1), which we will reset if the count is too low.
if (count > 500) {
(*glDepthRange)(0.0, 0.0);
(*glDepthFunc)(0x207);
}
else {
(*glDepthRange)(0.0, 1.0);
(*glDepthFunc)(0x203);
}With this, player models will now appear through walls and our wallhack is successful: